/**
 *  Copyright 2007-2008 University Of Southern California
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package edu.isi.pegasus.common.util;

import java.io.IOException;
import java.io.Writer;
import java.util.Stack;
import edu.isi.pegasus.common.logging.LogManager;
import edu.isi.pegasus.common.logging.LogManagerFactory;

/**
 *
 * @author gmehta
 */
public class XMLWriter {

    private Writer mWriter;
    private Stack<String> mStack;
    private StringBuffer mAttributes;
    private boolean mEmptyElement;
    private boolean mClosedElement;
    private boolean mWriteLine = true;
    private boolean mHeader = true;
    private LogManager mLogger;
    private static String START_ELEMENT_TAG = "<";
    private static String CLOSE_ELEMENT_TAG = ">";
    private static String START_END_ELEMENT_TAG = "</";
    private static String CLOSE_EMPTY_ELEMENT_TAG = "/>";
    private static String START_COMMENT_TAG = "<!-- ";
    private static String CLOSE_COMMENT_TAG = " -->";
    private static String INDENT = "   ";
    private static String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
    private String mLineSeparator;
    private String mNamespace;

    public XMLWriter(Writer writer) {
        this(writer, "");
    }

    public XMLWriter(Writer writer, String namespace) {
        mWriter = writer;

        mNamespace = (namespace == null) ? "" : namespace;
        mStack = new Stack<String>();
        mAttributes = new StringBuffer();
        mClosedElement = true;
        mLogger = LogManagerFactory.loadSingletonInstance();
        mLineSeparator = System.getProperty("line.separator", "\r\n");
        this.writeXMLHeader();
        this.writeXMLComment("generated on: " + Currently.iso8601(false));
        this.writeXMLComment("generated by: " + System.getProperties().getProperty("user.name", "unknown") + " [ " + System.getProperties().getProperty("user.region", "??") + " ]");
    }

    public XMLWriter startElement(String name) {
        startElement(name, 0);
        return this;
    }

    public XMLWriter startElement(String name, int indent) {
        try {
            //check if there are any previous elements open and close them
            closeElement();
            indent(indent);
            mClosedElement = false;
            mWriter.write(START_ELEMENT_TAG);
            if (!mNamespace.isEmpty()) {
                mWriter.write(mNamespace + ":");
            }
            mWriter.write(name);
        } catch (IOException ioe) {
            mLogger.log("Could not write element " + name + "using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);
        }
        mStack.add(name);
        mEmptyElement = true;
        return this;
    }

    public XMLWriter endElement() {
        endElement(0);
        return this;
    }

    public XMLWriter endElement(int indent) {
        if (mStack.empty()) {
            mLogger.log("No elements left to close",
                    LogManager.WARNING_MESSAGE_LEVEL);
        }
        String element = mStack.pop();
        try {
            if (element != null) {
                if (mEmptyElement) {
                    writeAttributes();
                    mWriter.write(CLOSE_EMPTY_ELEMENT_TAG);
                    if (mWriteLine) {
                        writeLine();
                    }
//                    mEmptyElement=false;
                } else {
                    indent(indent);
                    mWriter.write(START_END_ELEMENT_TAG);
                    if (!mNamespace.isEmpty()) {
                        mWriter.write(mNamespace + ":");
                    }

                    mWriter.write(element);
                    mWriter.write(CLOSE_ELEMENT_TAG);
                    if (mWriteLine) {
                        writeLine();
                    }
                }
                mClosedElement = true;
                mEmptyElement = false;
                mWriteLine = true;
            }
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not close element " + element + "using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);
        }
        return this;

    }

    public XMLWriter writeData(String data) {
        try {
            mEmptyElement = false;
            closeElement();
            mWriter.write(escapeXML(data));
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not write data for element " + mStack.peek() + " using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);

        }
        return this;
    }

    public XMLWriter writeUnEscapedData(String data) {
        try {
            mEmptyElement = false;
            closeElement();
            mWriter.write(data);
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not write data for element " + mStack.peek() + " using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);

        }
        return this;
    }

    public XMLWriter writeLine() {
        try {
            mWriter.write(mLineSeparator);
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not write empty line using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);

        }
        return this;
    }

    public XMLWriter noLine() {
        mWriteLine = false;
        return this;
    }

    public XMLWriter writeCData(String data) {
        try {
            mEmptyElement = false;

            closeElement();
            mWriter.write("<![CDATA[");
            mWriter.write(data);
            mWriter.write("]]>");
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not write data for element " + mStack.peek() + " using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);

        }
        return this;
    }

    public XMLWriter writeAttribute(String key, String value) {
        mAttributes.append(" ");
        if (!mNamespace.isEmpty()) {
            mAttributes.append(mNamespace).append(":");
        }
        mAttributes.append(key).append("=\"").append(
                escapeXML(value)).append("\"");
        return this;
    }

    /**
     * Writes out the attributes of a given element to the writer
     */
    private void writeAttributes() {

        try {
            mWriter.write(mAttributes.toString());
            mAttributes.setLength(0);
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not write attributes for element " + mStack.peek() + " using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);

        }
    }

    /**
     * Close open elements start tag. Write any attributes.
     * This is called when either a new child element is added to existing element or data is added.
     * @return XMLWriter
     */
    private void closeElement() {
        try {
            if (!mClosedElement) {
                writeAttributes();
                mClosedElement = true;
                mWriter.write(CLOSE_ELEMENT_TAG);
                if (mEmptyElement) {
                    writeLine();
                }
            }
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not close open element " + mStack.peek() + " using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);

        }
    }

    public XMLWriter writeXMLHeader() {
        if (mHeader) {
            try {
                mWriter.write(XML_HEADER);
                writeLine();
                mHeader = true;
            } catch (IOException ioe) {
                mLogger.log(
                        "Could not write xml header using XMLWriter",
                        LogManager.ERROR_MESSAGE_LEVEL);
                mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);
            }
        }

        return this;
    }

    public XMLWriter writeXMLComment(String comment, boolean linepadded) {

        try {
            closeElement();
            if (linepadded) {
                writeLine();
            }
            mWriter.write(START_COMMENT_TAG);
            mWriter.write(comment);
            mWriter.write(CLOSE_COMMENT_TAG);
            writeLine();
            if (linepadded) {
                writeLine();
            }
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not write xml comment using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);
        }
        return this;
    }

    public XMLWriter writeXMLComment(String comment) {
        this.writeXMLComment(comment, false);
        return this;
    }

    private XMLWriter indent(int indent) {
        try {
            mWriter.write(
                    (indent <= 0) ? "" : String.format(String.format("%%0%dd", indent), 0).replace("0", INDENT));
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not write xml comment using XMLWriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);
        }

        return this;

    }

    private static String escapeXML(String str) {
        String st = str;
        st = st.replaceAll("&", "&amp;");
        st = st.replaceAll("<", "&lt;");
        st = st.replaceAll(">", "&gt;");
        st = st.replaceAll("\"", "&quot;");
        st = st.replaceAll("'", "&apos;");
        return st;
    }

    public void close() {
        try {
            mWriter.close();
        } catch (IOException ioe) {
            mLogger.log(
                    "Could not close XMLwriter",
                    LogManager.ERROR_MESSAGE_LEVEL);
            mLogger.log(ioe.getMessage(), LogManager.DEBUG_MESSAGE_LEVEL);

        }
    }
}
